gRPC Support for Wolverine HTTP Endpoints + IMessageBus.StreamAsync<T>#2525
Merged
jeremydmiller merged 17 commits intoJasperFx:mainfrom Apr 20, 2026
Merged
Conversation
Planning documents live in .plans/grpc-streaming/ and should not leak into the eventual PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add IAsyncEnumerable<TResponse> StreamAsync<TResponse>() to ICommandBus and implement across all IMessageInvoker implementors. Handlers that return IAsyncEnumerable<T> are now streamed directly to the caller via StreamAsync, and typed async enumerables returned from regular InvokeAsync calls cascade each item individually (fixing a latent bug). - Add StreamAsync<T> to ICommandBus, IMessageInvoker, MessageBus - Implement Executor.StreamCoreAsync<T> with two-phase OTel span - Fix EnqueueCascadingAsync to iterate typed IAsyncEnumerable<T> (previously fell through to PublishAsync on the sequence object) - Add NotSupportedException for remote routes (MessageRoute, TopicRouting) - Add WolverineTracing streaming constants + StartStreaming helper - Add TestMessageContext.StreamAsync stubs (record + return empty stream) - Add SendingEnvelopeLifecycle.StreamAsync delegation - Add acceptance tests: stream items, empty, cancellation, cascade, options
Introduce WolverineFx.Http.Grpc package enabling Wolverine handlers to be exposed as ASP.NET Core gRPC services. Supports both proto-first (Grpc.Tools) and code-first (protobuf-net.Grpc) workflows with automatic service discovery, unary and server-streaming RPCs, and canonical exception mapping per Google AIP-193. - Add GrpcGraph discovery for proto-first stubs marked [WolverineGrpcService] - Add GrpcServiceChain code generation wrapping proto *Base classes - Add WolverineGrpcExceptionInterceptor mapping .NET exceptions to gRPC StatusCodes - Add WolverineGrpcServiceBase for code-first services forwarding to IMessageBus - Add MapWolverineGrpcServices() endpoint convention for automatic registration - Add integration tests covering unary, streaming, cancellation, and fault scenarios - Add documentation guide explaining both styles and exception semantics
Verify that Wolverine handler activities chain under the ASP.NET Core gRPC server hosting activity by sharing the same TraceId. Covers both code-first and proto-first service styles, unary and server-streaming. - Add WolverineActivityCapture test helper with global ActivityListener - Add otel_activity_propagation_tests for code-first unary/streaming - Add proto-first test case verifying same activity chain guarantee - Sort DiscoverSupportedMethods() alphabetically for byte-stable codegen - Add null-safe ParameterName() fallback for optimized assemblies - Add observability section to docs explaining Activity.Current preservation
…kers
Subscription-side IMessageInvoker implementations (NulloMessageInvoker,
InnerDataInvoker) now reject StreamAsync calls with NotSupportedException.
Subscriptions deliver events one-way; streaming request/response semantics
are not applicable and must be blocked to prevent future refactors from
accidentally relaxing this invariant.
- Add StreamAsync override throwing NotSupportedException to NulloMessageInvoker (Marten + Polecat)
- Add StreamAsync override throwing NotSupportedException to InnerDataInvoker (Marten + Polecat)
- Add subscription_invoker_streaming_guard_tests pinning the contract
- Add FaultingStreamRequest handler and mid-stream fault acceptance test
- Add TestMessageContext.StreamAsync tests for recording + empty sequence● ★ Insight ─────────────────────────────────────
- Why the 4 fixes are one-liners: Each subscription invoker already signaled "this isn't a bidirectional bus" by throwing NotSupportedException from InvokeAsync<T>. StreamAsync<T> extends exactly the same semantic — events arrive,
nothing comes back. One throwing line keeps the types consistent without pretending streaming works.
- Non-async throw from IAsyncEnumerable<T>: Because the body is an expression-bodied throw (not an iterator), we don't need async / yield. The method never returns an enumerable, so iterator lowering isn't triggered — the exception
surfaces immediately when called, not lazily on first MoveNextAsync. That's the correct behavior for a guard: fail fast, not at the first iteration.
- MartenTests.csproj has no explicit TargetFrameworks: multi-targeting net8/9/10 comes from a root-level props file (Directory.Build.props in src/Persistence or the repo root). Good to know for future test projects — don't duplicate
the TFM list.
Relocate proto-first Greeter and code-first Ping/Pong gRPC sample applications from test project to dedicated samples folder. Add bidirectional streaming sample (RacerWithGrpc) demonstrating IAsyncEnumerable parameter and return. Includes appsettings.json for each server and consistent project structure across all samples. - Move GreeterProtoFirstGrpc sample (proto-first unary + server-streaming + fault mapping) - Move PingPongWithGrpc sample (code-first unary) - Move PingPongWithGrpcStreaming sample (code-first server-streaming) - Add RacerWithGrpc sample (code-first bidirectional streaming) - Remove original test-embedded Ping/Greeter files from Wolverine.Http.Grpc.Tests
…ge type WolverineActivityCapture.ActivityStopped fires on arbitrary threads (Wolverine runtime + ASP.NET pipeline), racing with assertion reads. Lock all list access to prevent concurrent modification. Anchor assertion on messaging.message_type tag to isolate the exact request activity pair in parallel xUnit runs, filtering out background work and other test collections. - Add lock(_sync) around _all/_wolverine list mutations and snapshot reads - Change WolverineActivities/AllActivities to lock-guarded IReadOnlyList<T> - Rewrite AssertRequestActivityChainedUnderServerHostingActivity<T> to match on messaging.message_type tag - Update all test call sites with typed message parameter
Implement AIP-193 compliant rich error details for Wolverine-backed gRPC services via grpc-status-details-bin trailer. Includes pluggable IGrpcStatusDetailsProvider chain, automatic BadRequest mapping for FluentValidation failures, and GreeterWithGrpcErrors sample demonstrating validation and domain exception flows. - Add GrpcRichErrorDetailsConfiguration with MapException<T> inline builder - Add IGrpcStatusDetailsProvider pluggable error detail contributor chain - Add ValidationExceptionStatusDetailsProvider + IValidationFailureAdapter abstraction - Add DefaultErrorInfoProvider opt-in catch-all for unmapped exceptions - Add Wolverine.FluentValidation.Grpc package with FluentValidationFailureAdapter - Add GreeterWithGrpcErrors sample (server + client + shared Messages project) - Add UseGrpcRichErrorDetails() and UseFluentValidationGrpcErrorDetails() extensions - Add integration tests covering inline provider, default ErrorInfo, and validation flow
Contributor
Author
|
Hmm. Adding support for Google's However, it felt like it was worthwhile to give gRPC a similar treatment to HTTP with its FluentValidation capabilities and following the ProblemDetails specification. It's also worth mentioning I have some ideas brewing for other uses of the |
The package has no dependency on Wolverine.Http — gRPC is a peer edge
protocol, not a sub-feature of the HTTP framework. Rename restores that
distinction and aligns with sibling packages (Wolverine.Kafka,
Wolverine.SignalR, Wolverine.FluentValidation.Grpc).
- Namespace: Wolverine.Http.Grpc -> Wolverine.Grpc
- NuGet id: WolverineFx.Http.Grpc -> WolverineFx.Grpc
- Folders promoted out of src/Http/ to src/Wolverine.Grpc{,.Tests}/
- Solution: new /Grpc/ folder siblings to /Http/ and /Persistence/
- Docs moved to docs/guide/grpc.md with its own sidebar section
- Dependents updated: Wolverine.FluentValidation.Grpc, 5 sample apps,
root Wolverine InternalsVisibleTo entries
…rors, and samples Document both code-first and proto-first gRPC integration styles with Wolverine, including contract declaration patterns, handler flow, error handling with AIP-193 support, and detailed sample breakdowns. Covers unary and server-streaming RPCs, OpenTelemetry propagation, and rich error details via google.rpc.Status. - Add contracts.md explaining code-first vs proto-first decision matrix - Add handlers.md detailing service→bus→handler flow and discovery - Add errors.md covering default AIP-193 mapping + opt-in rich details - Add samples.md comparing 5 sample apps to grpc-dotnet equivalents - Document OpenTelemetry activity chain preservation in handlers.md - Document ValidationException→BadRequest bridge in errors.md
…w flag, and deferred features Add roadmap section to gRPC guide explaining what ships in the current PR vs. follow-up work. Covers MiddlewareScoping.Grpc behavior correction, codegen-preview --grpc flag for inspecting generated service chains, and deferred items: Validate convention returning Status?, code-first codegen parity with proto-first services, and hybrid handler shape design questions. - Add roadmap section with "Shipping in this PR" vs "Deferred to follow-up PRs" - Document MiddlewareScoping.Grpc as behavior correction (was MessageHandlers) - Document codegen-preview --grpc / -g flag mirroring --handler / --route - Defer Validate → Status? convention until code-first codegen lands - Defer code-first per-method codegen parity with proto-first path - Defer hybrid HTTP+gRPC+messaging handler shape (open design question)
Extend `wolverine-diagnostics codegen-preview` to preview generated gRPC service wrapper code via `--grpc <service>` flag. Accepts proto service name (`Greeter`), stub class name (`GreeterGrpcService`), or file name (`GreeterGrpcHandler`). Mirrors existing `--handler` and `--route` workflow. - Add GrpcFlag property and -g short alias to WolverineDiagnosticsInput - Add PreviewGrpcCode() searching ICodeFileCollection for gRPC chains - Add GrpcInputToFileName() normalizing input to expected file name - Add codegen_preview_grpc_tests end-to-end coverage in Wolverine.Grpc.Tests - Update docs/guide/command-line.md with gRPC preview examples
Change `GrpcServiceChain.Scoping` from `MessageHandlers` to `Grpc` so that middleware explicitly scoped to `[WolverineBefore(MiddlewareScoping.Grpc)]` attaches to gRPC chains and `[WolverineBefore(MiddlewareScoping.MessageHandlers)]` no longer inadvertently applies. - Add MiddlewareScoping.Grpc enum value for proto-first gRPC service chains - Change GrpcServiceChain.Scoping property to return MiddlewareScoping.Grpc - Add grpc_middleware_scoping_tests verifying scope matching behavior
Add `AddWolverineGrpcClient<T>()` to roadmap as an adoption-driven convenience layer over `Grpc.Net.ClientFactory`. Plans correlation-id/tenancy/message-id metadata propagation, `RpcException` → typed-exception client interceptor mirroring `WolverineGrpcExceptionInterceptor`, and `DeliveryOptions`-style header plumbing. Raw `GrpcChannel` + generated stubs remain fully supported.
…ption translation Implement `AddWolverineGrpcClient<T>()` extension providing correlation/tenant/message-id header propagation and `RpcException` → typed .NET exception translation. Supports both code-first (`[ServiceContract]`) and proto-first (generated `*Client`) contracts through unified API. Includes per-client `MapRpcException` override, `ConfigureChannel` escape hatch, and `PropagateEnvelopeHeaders` opt-out. - Add WolverineGrpcClientExtensions with AddWolverineGrpcClient<T> registration - Add WolverineGrpcClientPropagationInterceptor stamping envelope headers from IMessageContext - Add WolverineGrpcClientExceptionInterceptor translating RpcException per AIP-193 table - Add WolverineGrpcExceptionMapper.MapToException inverse mapping (client-side) - Add WolverineGrpcCodeFirstChannelFactory for code-first contract substrate - Add IHeaderEchoService test contract + HeaderEchoGrpcService test endpoint - Add propagation_interceptor_tests, exception_interceptor_tests, registration_tests - Add docs/guide/grpc/client.md covering registration, propagation, error translation
…typed-exception round-trip across Wolverine gRPC services
…mand-line support Add comprehensive README documentation for all five gRPC samples (GreeterProtoFirstGrpc, OrderChainWithGrpc, PingPongWithGrpc, PingPongWithGrpcStreaming, RacerWithGrpc) covering architecture, running instructions, and expected output. Enable `RunJasperFxCommands()` on all sample servers for consistency with existing samples. Standardize framework flag usage across GreeterWithGrpcErrors sample. - Add README.md files to GreeterProtoFirstGrpc, OrderChainWithGrpc, PingPongWithGrpc, PingPongWithGrpcStreaming, and RacerWithGrpc samples - Replace app.Run() with RunJasperFxCommands(args) in all sample server Program.cs files - Add --framework net9.0 flag to GreeterWithGrpcErrors running instructions for consistency
jeremydmiller
added a commit
that referenced
this pull request
Apr 20, 2026
gRPC #2525 follow-up: Correctness Gaps
This was referenced Apr 21, 2026
This was referenced Apr 23, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: gRPC services + typed clients +
IMessageBus.StreamAsync<T>+ rich error detailsAdds a new
WolverineFx.Grpcpackage that exposes Wolverine handlers asASP.NET Core gRPC services, plus a first-class
IMessageBus.StreamAsync<T>overload so handlers can return
IAsyncEnumerable<T>. The same handler can nowback a REST endpoint, an async message, and a gRPC call with no duplication.
Also ships an opt-in
google.rpc.Statusrich-error pipeline (the gRPCcounterpart to
ProblemDetails) with a separateWolverineFx.FluentValidation.Grpcbridge package, and a Wolverine-flavored typed gRPC client
(
AddWolverineGrpcClient<T>()) that stamps envelope headers on outgoing callsand translates
RpcException→ typed .NET exceptions — closing the loop so aWolverine → Wolverine gRPC hop round-trips correlation-id / tenant-id and
typed failures with zero user plumbing.
Branch:
feature/grpc-and-streaming-supportTarget:
mainScope: 16 commits, +8444 / −9 across 154 files. 78 tests green in
Wolverine.Grpc.Tests.Why
gRPC is a recurring ask for teams already running Wolverine as their in-process
mediator — they want the same handlers addressable over a strongly-typed wire
protocol, especially for streaming. The alternative (hand-rolling gRPC stubs
that forward to
IMessageBus) isn't hard, but it's boilerplate at every methodand duplicates the exception mapping, discovery, and codegen story Wolverine
already owns for HTTP.
This PR makes gRPC a peer of
Wolverine.Http: handler-first, convention-driven,codegen-participating, and observability-correct out of the box.
What's in the package
IMessageBus.StreamAsync<T>(M2)A new overload on
IMessageBus:Any handler that returns
IAsyncEnumerable<T>is routed to it. The executor,tracing executor, message context, and routing layers all grew the matching
method so the streaming path shares the full Wolverine pipeline (activities,
middleware, error handling) with unary
InvokeAsync<T>.Files:
Wolverine/IMessageBus.cs,Runtime/MessageBus.cs,Runtime/MessageContext.cs,Runtime/Handlers/{Executor,TracingExecutor,NoHandlerExecutor}.cs,Runtime/Routing/{IMessageInvoker,MessageRoute,TopicRouting}.cs,Runtime/WolverineRuntime.cs,Runtime/WolverineTracing.cs,TestMessageContext.cs,Transports/Sending/SendingEnvelopeLifecycle.cs.Code-first services (M3)
WolverineGrpcServiceBasegives you an injectedIMessageBus Bus; the methodbody is a one-liner to
Bus.InvokeAsync<T>orBus.StreamAsync<T>.Discovery: any class whose name ends in
GrpcService, or carries[WolverineGrpcService], is picked up byMapWolverineGrpcServices().Proto-first services (M4)
Ship a
.proto, letGrpc.Toolsgenerate the stub, mark an abstract subclasswith
[WolverineGrpcService]— Wolverine generates a concrete wrapper named{ProtoServiceName}GrpcHandlerand forwards each RPC toIMessageBus.chains (
GrpcGraphimplementsICodeFileCollectionWithServices+IDescribeMyself) — it shows up indotnet run -- describe/describe-routing.[WolverineGrpcService]throws
InvalidOperationExceptionat startup with the offending type name anda pointer to its proto base. Client-streaming and bidirectional shapes also
fail fast with a clear message rather than silently skipping.
Files:
GrpcGraph.cs,GrpcServiceChain.cs,WolverineGrpcServiceAttribute.cs,ModifyGrpcServiceChainAttribute.cs,WolverineGrpcExtensions.cs.Exception mapping (AIP-193, M5)
WolverineGrpcExceptionInterceptoris registered automatically byAddWolverineGrpc(). Ordinary .NET exceptions map to the canonical gRPC statuscode per google.aip.dev/193:
OperationCanceledExceptionCancelledTimeoutExceptionDeadlineExceededArgumentException(+subclasses)InvalidArgumentKeyNotFoundException/FileNotFoundException/DirectoryNotFoundExceptionNotFoundUnauthorizedAccessExceptionPermissionDeniedInvalidOperationExceptionFailedPreconditionNotImplementedException/NotSupportedExceptionUnimplementedRpcExceptionInternalWolverineGrpcExceptionMapper.Map(ex)is public for reuse in user interceptors.Rich error details (M11)
Opt-in AIP-193
google.rpc.Statuspayloads packedinto the
grpc-status-details-bintrailer — the gRPC counterpart to HTTP'sProblemDetails/ValidationProblemDetails. Off unless the user explicitlycalls
UseGrpcRichErrorDetails(); when off,WolverineGrpcExceptionInterceptorfalls through to the canonical M5 table exactly as before.
Design choices, all deliberately mirroring existing Wolverine idioms:
UseGrpcRichErrorDetailsguards with aWolverineGrpcRichDetailsMarkerDI registration, the same patternUseFluentValidationuses. Calling it twice is a no-op; calling it from alibrary extension is safe.
UseGrpcRichErrorDetails(cfg => cfg.MapException<T>(...).EnableDefaultErrorInfo())follows the 2026 Wolverine convention for opts-scoped configuration (vs. a
singleton
WolverineGrpcOptionssidecar). The builder is ephemeral — itdecomposes into
IServiceCollectionregistrations when the extension returns.ServerCallContext.GetHttpContext().RequestServices, so custom providers withscoped dependencies work the same as any ASP.NET Core service. No reflection
on the exception type — providers claim exceptions via
CanHandle.IValidationFailureAdapter(library-specific exception →FieldViolations)feeds a single
ValidationExceptionStatusDetailsProviderthat owns theBadRequestpacking. That keeps the bridge package surface tiny (one class)and lets a future DataAnnotations bridge plug in without touching core.
DefaultErrorInfoProvideris opt-in and opaque. EmitsErrorInfo { Reason = exception.GetType().Name, Domain = "wolverine.grpc" }—no messages, no stack traces. Safe to turn on in production.
Core seam (stays in
Wolverine.Grpc):Built-in providers:
ValidationExceptionStatusDetailsProvider(chainsIValidationFailureAdapters),InlineStatusDetailsProvider<TException>(backsMapException<T>),DefaultErrorInfoProvider(opt-in catch-all).Files:
IGrpcStatusDetailsProvider.cs,IValidationFailureAdapter.cs,ValidationExceptionStatusDetailsProvider.cs,DefaultErrorInfoProvider.cs,GrpcRichErrorDetailsConfiguration.cs,GrpcRichErrorDetailsExtensions.cs,and a
TryBuildRichStatusbranch added toWolverineGrpcExceptionInterceptor.cs.Wolverine.FluentValidation.Grpcbridge package (M11)The FluentValidation ↔
BadRequestadapter ships in a separate package sohosts that don't use FluentValidation never pull the dependency — mirroring the
Wolverine.Http.FluentValidation/Wolverine.Httpsplit on the HTTP side.Contents (three small files):
FluentValidationFailureAdapter— claimsFluentValidation.ValidationException,maps
ValidationFailure.PropertyName → BadRequest.FieldViolation.FieldandErrorMessage → Description.WolverineFluentValidationGrpcMarker+UseFluentValidationGrpcErrorDetails—marker-guarded idempotent opt-in.
No reflection, no exception sniffing in core — the adapter is the plug.
Files:
src/Extensions/Wolverine.FluentValidation.Grpc/*(three source files +csproj, PackageId
WolverineFx.FluentValidation.Grpc).Observability (M6)
The gRPC adapter preserves
Activity.Currentacross the gRPC → Wolverineboundary, so every handler activity chains under the ASP.NET Core hosting
activity (
Microsoft.AspNetCore.Hosting.HttpRequestIn) under a singleTraceId.Users register the
"Wolverine"ActivitySourceon their OTel pipeline andget end-to-end traces for free.
Known testability gap (documented):
Microsoft.AspNetCore.TestHost.TestServerbypasses real HTTP/2, so client→server
traceparentheader propagation isn'tobservable via the in-memory fixture. The OTel tests in this PR assert only the
server-side chain Wolverine actually owns; the doc guide calls out
WebApplicationFactorywith a loopback port as the right tool for end-to-endpropagation assertions.
Codegen polish (M6)
GrpcServiceChain.DiscoverSupportedMethodssorts method list bystring.CompareOrdinal— reflection'sGetMethods()order is unspecified,and byte-stable generated source keeps diffs clean across runs. Regression
test:
discovered_methods_are_sorted_alphabetically_for_byte_stable_codegen.ForwardUnaryToMessageBusFrame.ParameterName(parameters, i)replaces rawparameters[i].Name!so a stripped-metadata assembly produces a readableargNfallback instead of an NRE.Diagnostics CLI —
codegen-preview --grpc(M12)wolverine-diagnostics codegen-previewgains a--grpc/-gflag alongsidethe existing
--handler/-hand--route/-r, so proto-first gRPCservice wrappers can be inspected one at a time without dumping the whole
codegen output. Accepts the proto service name (
Greeter), the stub classname (
GreeterGrpcService), or the generated file name (GreeterGrpcHandler).Implementation walks
services.GetServices<ICodeFileCollection>()— the sameDI seam HTTP uses — so
Wolverine.Grpckeeps zero compile-time coupling tocore
Wolverineand the command stays symmetric across the three entry-pointstyles. Covered end-to-end by
codegen_preview_grpc_testsinWolverine.Grpc.Tests, plus helper-level string tests forGrpcInputToFileNamein
CoreTests.Middleware scoping —
MiddlewareScoping.Grpc(M13)The existing
MiddlewareScopingenum (Anywhere,MessageHandlers,HttpEndpoints)grows a new
Grpcvalue, andGrpcServiceChain.Scopingchanges fromMessageHandlerstoGrpc. This is a behavior correction: previously, anyattribute scoped to
MessageHandlers— e.g.[WolverineBefore(MiddlewareScoping.MessageHandlers)]—silently over-attached to gRPC service chains because those chains reported themselves
as message handlers. With this change:
[WolverineBefore(MiddlewareScoping.Grpc)]becomes available for gRPC-onlycross-cutting concerns and attaches exclusively to gRPC chains.
[WolverineBefore(MiddlewareScoping.MessageHandlers)]now stays out of gRPC chains.[WolverineBefore(MiddlewareScoping.Anywhere)](the default) still applies everywhere.The new enum value is appended last so existing ordinals (
MessageHandlers = 1,HttpEndpoints = 2) are preserved — no reinterpretation risk for any serialized orreflected attribute value.
ChainExtensions.MatchesScopeuses direct equality afteran
Anywhereshort-circuit, so the additional enum value is fully transparent toall existing consumers.
Covered by
grpc_middleware_scoping_testsinWolverine.Grpc.Tests(4 tests —scoping value assertion,
Grpc-scoped applies,Anywherestill applies,MessageHandlersno longer applies against a realGrpcServiceChaindiscoveredfrom the Greeter proto-first sample).
Files:
src/Wolverine/Attributes/HandlerMethodAttributes.cs,src/Wolverine.Grpc/GrpcServiceChain.cs.Typed gRPC client —
AddWolverineGrpcClient<T>()(M14)Promoted from the "tentative roadmap" bullet into shipping scope. A thin
Wolverine wrapper over
Grpc.Net.ClientFactory.AddGrpcClient<T>()that closesthe symmetry with the server-side interceptors: envelope identity headers flow
outbound and
RpcExceptions come back as typed .NET exceptions.Three conveniences layered on top of the Microsoft client factory — no
replacement, only additive:
WolverineGrpcClientPropagationInterceptor— stampscorrelation-id,tenant-id,parent-id,conversation-id,message-idon outgoing callswhen an
IMessageContextis resolvable from the current DI scope. The wirevocabulary matches
EnvelopeConstants— the same kebab-case keys everyother Wolverine transport uses via
EnvelopeMapper<TIncoming,TOutgoing>.Silently no-ops when no
IMessageContextis in scope (bareProgram.cscallers) and never overwrites a header the caller already set on the
per-call
Metadata(per-call overrides win).WolverineGrpcClientExceptionInterceptor— translates an incomingRpcExceptioninto a typed .NET exception using the sharedWolverineGrpcExceptionMapper.MapToExceptiontable (the inverse of theserver-side
WolverineGrpcExceptionInterceptor). Streaming calls wrap eachstream reader with a
MappingStreamReader<T>so translation fires onMoveNextrather than the outer call.WolverineGrpcClientBuilder.ConfigureChannel— escape hatch to rawGrpcChannelOptions. Wolverine wraps, never replaces, the vendor API.Ordering invariant (load-bearing). The exception interceptor is registered
first so it sits outermost in the call chain. When Polly or
AddStandardResilienceHandler()is composed on the same typed client, itsretry loop fires inside the
RpcExceptioncatch — translating to a typedexception before the retry loop would silently defeat retry semantics.
Documented on the interceptor and pinned by unit tests.
Unified surface, two substrates.
AddWolverineGrpcClient<T>()auto-detectsthe contract style and picks the right registration path:
Greeter.GreeterClient)→ delegates to
AddGrpcClient<T>();WolverineGrpcClientBuilder.HttpClientBuilderis non-null so Polly, logging, and authentication handlers compose normally.
[ServiceContract]-annotated interface forprotobuf-net.Grpc)→ uses an internal
WolverineGrpcCodeFirstChannelFactory, becauseprotobuf-net.Grpcdoes not ride onIHttpClientFactory;HttpClientBuilderis
nullin that case (callers who need Polly must use proto-first).The delegate hook
WolverineGrpcClientOptions.MapRpcExceptionis a per-clientoverride consulted before the default table — returning
nullfalls through.PropagateEnvelopeHeaders(defaulttrue) toggles the propagation interceptoroff when an
IMessageContextis resolvable but propagation is undesired.Files:
src/Wolverine.Grpc/Client/WolverineGrpcClientExtensions.cs,WolverineGrpcClientOptions.cs,WolverineGrpcClientBuilder.cs,WolverineGrpcClientPropagationInterceptor.cs,WolverineGrpcClientExceptionInterceptor.cs,WolverineGrpcCodeFirstChannelFactory.cs,WolverineGrpcCodeFirstClientOptions.cs, plus a smallWolverineGrpcExceptionMapper.MapToExceptionreverse-table addition. Guidepage:
docs/guide/grpc/client.md.Interface completion (CI fix)
M2 extended
IMessageInvokerwithStreamAsync<T>(object, MessageBus, …), butfour subscription-side implementers that don't inherit from
MessageBusweremissed — surfaced by CI as
CS0535compile errors:Wolverine.Marten.Subscriptions.InnerDataInvoker<T>Wolverine.Marten.Subscriptions.NulloMessageInvokerWolverine.Polecat.Subscriptions.InnerDataInvoker<T>Wolverine.Polecat.Subscriptions.NulloMessageInvokerAll four now implement
StreamAsync<T>asthrow new NotSupportedException(),matching their existing
InvokeAsync<T>semantics: subscriptions deliver eventsone-way, so request/response invocation doesn't apply and streaming even less
so. Fast, expression-bodied throws — no iterator lowering, so the exception
surfaces on call rather than lazily on first
MoveNextAsync.Pinned with two guard tests (one per package) so a future refactor can't
silently relax the invariant.
Runnable samples (M10 + M11)
The canonical service + handler pairs live in proper sample projects under
src/Samples/, following the existingPingPong/PingPongWithRabbitMqfolder convention. Six sample trios, eighteen projects in total:
PingPongWithGrpc/{Messages,Ponger,Pinger}— code-first unary over Kestrel port 5001.PingPongWithGrpcStreaming/{Messages,Ponger,Pinger}— code-first server streaming over 5002.GreeterProtoFirstGrpc/{Messages,Server,Client}— proto-first (unary + streaming + fault mapping) over 5003.RacerWithGrpc/{RacerContracts,RacerServer,RacerClient}— code-first bidirectional streamingover 5004, ported from the closed March PR. Client pushes
IAsyncEnumerable<RacerUpdate>; serverbridges each item through
IMessageBus.StreamAsync<RacePosition>and yields back the fullleaderboard — the recommended pattern until a first-class
StreamAsync<TReq, TResp>lands.GreeterWithGrpcErrors/{Messages,Server,Client}(M11) — code-first rich error detailsover 5005. Two RPCs cover the two supported paths:
GreetsurfacesFluentValidation.ValidationExceptionasBadRequestwithFieldViolations, andFarewellthrows a domain
GreetingForbiddenExceptionthat the server maps inline toPreconditionFailurevia
MapException<T>. The client demonstrates the intended read pattern —RpcException.GetRpcStatus()+Any.Unpack<T>()againstBadRequest.Descriptor/PreconditionFailure.Descriptor. Smoke-tested end-to-end while writing the sample.OrderChainWithGrpc/{Contracts,OrderServer,InventoryServer,OrderClient}(M14) — aWolverine → Wolverine chain demonstrating the typed client end-to-end. An external
(vanilla
grpc-dotnet) client callsOrderServer;PlaceOrderHandlerasks forIInventoryServiceby DI — it's registered viaAddWolverineGrpcClient<IInventoryService>()in
Program.cs— and callsinventory.Reserve(...)with no manualMetadataorCallOptions.The propagation interceptor stamps
correlation-id/tenant-id/parent-id/conversation-id/message-idautomatically;InventoryServer's handler reads theupstream correlation-id off its own
IMessageContextand echoes it back on the reply, so thesample asserts the same correlation-id reached both hops. The failure path throws
KeyNotFoundExceptionfrom the inventory handler — it surfaces asKeyNotFoundExceptionat the upstream handler's call site (client-side exceptioninterceptor doing the inverse mapping), and the upstream server-side interceptor
re-maps it to
NotFoundfor the external caller. Four-project trio (the extra projectis the second server) listening on 5006 (OrderServer) and 5007 (InventoryServer).
The test project takes
ProjectReferences to the sample Messages + Server/Ponger projectsinstead of duplicating the canonical types — handler + contract definitions are single-sourced.
GrpcTestFixtureandProtoFirstGrpcFixturedeclare the sample server assemblies as theapplication assemblies for discovery. M11 kept the rich-errors fixture (
RichErrorsCodeFirstFixture)inside the test project on purpose: its contracts are deliberately minimal and focused on
exercising the full validation →
BadRequestpipeline rather than doubling as a sample.Package bump:
Grpc.Core.Api2.76.0 added toDirectory.Packages.props— required because theshared Messages assemblies with
<Protobuf GrpcServices="Both"/>needServerServiceDefinition,ServiceBinderBase, andMethod<TRequest,TResponse>at compile time for the generated code.Public API surface
AddWolverineGrpc()MapWolverineGrpcServices()WolverineGrpcServiceBaseIMessageBus Bus[WolverineGrpcService]GrpcService-suffixed classesWolverineGrpcExceptionMapper.Map(ex)WolverineGrpcExceptionInterceptorIMessageBus.StreamAsync<T>(request, ct)opts.UseGrpcRichErrorDetails(cfg => ...)google.rpc.Statuspipeline (M11)opts.UseFluentValidationGrpcErrorDetails()ValidationException→BadRequestbridge (M11, separate package)IGrpcStatusDetailsProvidergoogle.rpc.Status(M11)IValidationFailureAdapterFieldViolationmapping (M11)GrpcRichErrorDetailsConfiguration.MapException<T>(code, factory)GrpcRichErrorDetailsConfiguration.AddProvider<TProvider>()GrpcRichErrorDetailsConfiguration.EnableDefaultErrorInfo()ErrorInfo(M11)wolverine-diagnostics codegen-preview --grpc <service>MiddlewareScoping.Grpc[WolverineBefore/After/Finally/OnException]to scope middleware exclusively to gRPC service chains (M13)services.AddWolverineGrpcClient<T>(o => o.Address = ...)RpcException→ typed-exception translation (M14)WolverineGrpcClientOptions.{Address, PropagateEnvelopeHeaders, MapRpcException}WolverineGrpcClientBuilder.ConfigureChannel(Action<GrpcChannelOptions>).HttpClientBuilderexposes the underlyingIHttpClientBuilderfor proto-first clients (M14)WolverineGrpcClientPropagationInterceptor/WolverineGrpcClientExceptionInterceptorWolverineGrpcExceptionMapper.MapToException(RpcException)Map(Exception)(M14)Tests (78 green in
Wolverine.Grpc.Tests)Acceptance/streaming_handler_support.cs— 6 tests covering happy path, cancellation, handler-side exceptions, middleware integration.code_first_grpc_tests.cs— unary round-trip, server-streaming round-trip, mid-stream cancellation, DI registration, exception-mapping theory (8 cases covering the full AIP-193 table).proto_first_grpc_tests.cs— unary round-trip, multiple methods, generated-wrapper naming convention, server-streaming round-trip, mid-stream cancellation, exception-mapping theory (6 cases), codegen ordering regression,GrpcGraphregistered onOptions.Partsfor CLI diagnostics; plus discovery unit tests for abstract-vs-concrete stub classification.exception_mapping_integration_tests.cs— 8 unary cases plus a streaming-after-first-yield case.otel_activity_propagation_tests.cs+ proto-first equivalent — 3 cases asserting Wolverine activityTraceIdmatches the ASP.NET Core hosting activity. (Post-M10 this suite was hardened against a race betweenActivityStoppedand assertion reads by locking all list access and anchoring assertions on themessaging.message_typetag — parallel xUnit runs no longer see intermittent failures.)RichErrors/— 10 tests across four files:DefaultErrorInfoProviderTests(3 — code emission, reason/domain tagging, no message/stack leakage),ValidationExceptionStatusDetailsProviderTests(3 — null-when-no-match, first-match-wins, multi-violation),InlineStatusDetailsProviderTests(2 — type-mismatch null, code + packed payload), andrich_error_details_code_first_tests(2 — end-to-end FluentValidation round-trip and valid-request passthrough viaRichErrorsCodeFirstFixture).codegen_preview_grpc_tests(2 — end-to-end codegen against theGreeterGrpcHandlerchain discovered from the proto-first sample, plus the unknown-input no-match path) inWolverine.Grpc.Tests, plus 6GrpcInputToFileNametheory cases inCoreTests/Diagnostics/WolverineDiagnosticsCommandTests.cs(bare proto name, stub class name, already-normalized file name, case-preserving, whitespace trim).grpc_middleware_scoping_testsinWolverine.Grpc.Tests— 4 tests asserting (1)GrpcServiceChain.Scoping == MiddlewareScoping.Grpc, (2)[WolverineBefore(MiddlewareScoping.Grpc)]applies viaChainExtensions.MatchesScope, (3)[WolverineBefore](Anywhere) still applies, (4)[WolverineBefore(MiddlewareScoping.MessageHandlers)]no longer applies. All four run against a realGrpcServiceChaindiscovered from the Greeter proto-first sample.Client/— 19 tests across four files, all running against a live Kestrel HTTP/2 loopback inWolverineGrpcClientFixture.registration_tests(7) — code-first[ServiceContract]interface classified as code-first, proto-first generated client class not classified as code-first, code-first registration returns a code-first builder (nullHttpClientBuilder), proto-first registration exposes anIHttpClientBuilder, end-to-end code-first unary round-trip, end-to-end proto-first unary round-trip, missingAddressthrows a clear error at DI resolution time.propagation_interceptor_tests(4) — stampscorrelation-id+tenant-idwhenIMessageContextis in scope, stamps envelope-derived headers (parent-id/conversation-id/message-id) when the context carries an envelope, silently no-ops when noIMessageContextis resolvable,PropagateEnvelopeHeaders = falsedisables stamping per client.exception_interceptor_tests(5) — unaryRpcException→ typed .NET exception translation theory (canonical AIP-193 codes), unmapped status code passes through the originalRpcException, server-streamingRpcExceptionafter the first yield is translated per-MoveNext, per-clientMapRpcExceptionoverride takes precedence over the default table, override returningnullfalls through to the default table.exception_mapper_reverse_tests(3) —MapToExceptiontheory for known codes mapping to idiomatic .NET exceptions, unmapped codes return the originalRpcException, originalRpcExceptionpreserved on the inner exception for diagnostics.TestMessageContext.StreamAsync(coverage gap):TestMessageContextTests.cs— 2 cases covering the plain andDeliveryOptionsoverloads of the user-facing test-spy API.subscription_invoker_streaming_guard_tests.csinMartenTests+PolecatTests— assertsNulloMessageInvoker.StreamAsync<T>throwsNotSupportedException.Documentation
Guide:
docs/guide/grpc/(multi-page section, own top-level sidebar entry gRPC Services):index.md— rationale, getting started, runnable-samples callout, API reference table,Current Limitations, and a Roadmap section split into "Shipping in this PR"
(
MiddlewareScoping.Grpc,codegen-preview --grpc) and "Deferred to follow-up PRs"(
Validate→Status?convention, code-first codegen parity, hybrid handler shape) socontributors and consumers can plan against it.
handlers.md— the service →IMessageBus→ handler flow; how gRPC handlers differfrom HTTP/messaging and how OTel traces survive the hop.
contracts.md— code-first vs proto-first, side by side.errors.md— full AIP-193Exception → StatusCodetable, opt-ingoogle.rpc.Statuspipeline (M11 wiring, validation path, domain-exception
MapException, customIGrpcStatusDetailsProvider, opt-inErrorInfocatch-all, client read pattern, andthe ~8 KB trailer-budget caveat).
streaming.md— server streaming, the bidirectional bridge pattern, cancellation.samples.md— the five sample trios with pointers to the equivalent officialgrpc-dotnetexamples for comparison.docs/guide/samples.mdlists the runnable gRPC sample trios undersrc/Samples/.docs/guide/command-line.mddocuments the newcodegen-preview --grpcflag with examples.Vitepress sidebar entry: top-level gRPC Services section with five child pages.
Packages added / bumped
Grpc.AspNetCore2.76.0 (new) — server hostingGrpc.Net.Client2.76.0 (new) — test clientGrpc.Tools2.72 → 2.76 (bumped to match)Grpc.Core.Api2.76.0 (new, for sample Messages projects withGrpcServices="Both")protobuf-net.Grpc1.2.2 (new) — code-first contractsprotobuf-net.Grpc.AspNetCore1.2.2 (new) — code-first server wiringGoogle.Api.CommonProtos2.16.0 (new, M11) —google.rpc.{Status,Code,BadRequest,ErrorInfo,PreconditionFailure}Grpc.StatusProto2.76.0 (new, M11) —RpcException.GetRpcStatus()/Status.ToRpcException()extensionsNew NuGet packages published from this repo:
WolverineFx.Grpc— the integration package itself. (Renamed from the originallyproposed
WolverineFx.Http.Grpcper PR gRPC Support for Wolverine HTTP Endpoints + IMessageBus.StreamAsync<T> #2525 review — the package has no codedependency on
Wolverine.Http; the old name only reflected the ASP.NET Core hostingrelationship and was confusing. Project moved from
src/Http/Wolverine.Http.Grpc/to
src/Wolverine.Grpc/; tests moved in parallel.)WolverineFx.FluentValidation.Grpc(M11) — opt-in bridge. Depends onWolverineFx.Grpc+WolverineFx.FluentValidation; carries no core surface.Explicitly deferred
Called out in
docs/guide/grpc/index.mdunder Current Limitations (hard blockerstoday) and Roadmap (planned follow-ups):
Current Limitations
shape — blocked on request-side
IAsyncEnumerable<TRequest>overloads onIMessageBus. Proto stubs with these shapes fail fast at startup rather thansilently skipping. Code-first bidi is still achievable today by bridging each
client item through
Bus.StreamAsync<TResponse>in the service layer —demonstrated in the
RacerWithGrpcsample.Exception → StatusCodemapping is static; a follow-up will make the fallback table pluggable. Note:
this is orthogonal to the M11 rich-details pipeline, which is already
user-configurable via
MapException<T>,IGrpcStatusDetailsProvider, andIValidationFailureAdapter.Roadmap (follow-up PRs)
Validateconvention →Status?— HTTP handlers already support an opt-inValidateshort-circuit; the gRPC equivalent would returnGrpc.Core.Status?(or
google.rpc.Status). Deferred because it lands cleanest on top of code-firstcodegen parity below.
GrpcServiceChainWolverineGrpcServiceBasepath) currently resolve dependencies via service location inside each method.
Generating per-method code files for code-first services is the prerequisite for
the
Validateconvention above and for tighter Lamar/MSDI optimization.question around naming, scoping, and method-name conflicts. No concrete plan yet.
ValidationExceptionbridge — theIValidationFailureAdapterseam exists, but only the FluentValidation adapter ships in this PR. A
DataAnnotations adapter is a drop-in follow-up (one class, no core changes).
Review guidance
IMessageBus.StreamAsync<T>is the one change to the Wolverine core surface;everything else is additive in new packages.
abstractproto-stub requirement is a load-bearing design decision (thegenerated wrapper is the concrete implementation). The doc + fail-fast
diagnostic are the two guardrails; please flag if you want a different shape.
WolverineFx.FluentValidation.Grpcis a separatepackage rather than a
#ifinsideWolverineFx.Grpc, mirroring theWolverineFx.Http.FluentValidation/WolverineFx.Httpsplit. If thepreference is "one package with conditional deps" instead, the adapter is
three files and trivial to fold in.
UseGrpcRichErrorDetailsuses the sameIServiceCollection.AddSingleton<Marker>idempotency pattern asUseFluentValidation, and the per-callGrpcRichErrorDetailsConfigurationbuilder is ephemeral (decomposed into registrations on exit). If you'd prefer
a long-lived
WolverineGrpcOptionssidecar instead, flag it — I picked thisshape because it matched the 2026 method-on-config convention we've been
standardising on.
codegen-preview --grpcgoes through the genericICodeFileCollectionDI seam rather than taking a compile-time dependency onWolverine.Grpcfrom core. Same indirection--routeuses for HTTP. KeepsWolverine.csprojgRPC-package-free.MiddlewareScoping.Grpcis appended last(ordinal
3) rather than inserted alphabetically, soMessageHandlers = 1and
HttpEndpoints = 2retain their existing values. Flag if you'd preferalphabetical ordering despite the ordinal shift; this is a load-bearing
choice for any attribute-value reflection/serialization paths downstream.
Wolverine.Grpcexposes internals toWolverine.Grpc.Tests, mirroringWolverine.Http'sAssemblyAttributes.cs. Needed so the M11 unit tests can reach the internalInlineStatusDetailsProvider<T>andGrpcRichErrorDetailsConfiguration.Registrationswithout promoting those to the public surface.
first on the client so it sits outermost in the call chain. When Polly /
AddStandardResilienceHandler()is composed on the same typed client, theretry loop fires inside the
RpcExceptioncatch — translating to a typedexception before retry would silently defeat retry semantics. Flag if you'd
prefer the opposite ordering; doc + interceptor both carry the invariant.
AddGrpcClient<T>()(fullIHttpClientFactorystory); code-first(
[ServiceContract]interfaces forprotobuf-net.Grpc) use an internalWolverineGrpcCodeFirstChannelFactorybecauseprotobuf-net.Grpcdoes notride on
IHttpClientFactory.WolverineGrpcClientBuilder.HttpClientBuilderis
nullfor code-first clients — documented, and the sampleOrderChainWithGrpcexercises the code-first path end-to-end.